# -*- coding:utf-8 -*-
from io import BytesIO
from queue import Queue
from threading import Thread
# http 请求
from flask import Flask, request, json
from urllib3 import encode_multipart_formdata
from urllib import parse
import time
import datetime
import logging
import requests
import yaml
import copy
import pytz
import uuid

import cloudlaba

logging.basicConfig(filename='./dahua_msg_process_logger.log', format="%(asctime)s|%(message)s",
                    datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO)

# 设置App name
app = Flask(__name__)

# ========================================多线程处理变量定义================================================

# 用于按次序记录来自大华摄像枪对应的消息id，msgQueue左进右出，并转入处理队列
msgQueue = Queue(maxsize=0)

# 按次序顺序处理消息队列中的消息id
msgProcess = Queue(maxsize=0)

# 不需要消息id，直接获取告警ID的情况
alarmMsg = {}

# 摄像枪在大华平台上对应的client_id，client_secret信息
camManageDict = {}

# 记录client_id对应的access_token的有效时间
clientAccessToken = {}

# 大华摄像枪的id信息与视频云的摄像枪id，主设备id之间的映射关系
camIdDict = {}

# 大华的告警消息映射到视频云中的告警类型
dahuaMsgType = {}

# 移动视频云接口url与端口信息
gmcc_url_port = ""

# 休眠时间，用于队列处理的时候使用
sleep_time = 60

# 云喇叭对应的接口URL前缀信息
cloud_horn_url = ""

# 任务对应的云喇叭设备相关的id信息，一个任务可以对应多个云喇叭，多个云喇叭信息之间分号隔开
task_cloud_horns_dict = {}

# 摄像枪ID对应的AI能力类型，以及相应的消息播报任务ID，任务ID需要在云喇叭的云平台提前配置好，并与喇叭进行关联
camIdCloudHornDict = {}

# 云喇叭执行任务时的固定休眠时间
cloudhorn_sleep_time = 15


# ========================================多线程处理类与函数定义================================================
def readYaml():
    with open('configure.yaml', encoding='utf-8') as f:
        data = yaml.load(f, Loader=yaml.SafeLoader)
    # print(data)
    global camManageDict
    camManageDict = copy.deepcopy(data['camManageDict'])

    global camIdDict
    camIdDict = copy.deepcopy(data['camIdDict'])

    global dahuaMsgType
    dahuaMsgType = copy.deepcopy(data['dahuaMsgType'])

    global gmcc_url_port
    gmcc_url_port = data['gmcc_url_port']

    global sleep_time
    sleep_time = data['sleep_time']

    # 云喇叭相关配置参数
    global cloud_horn_url
    cloud_horn_url = data['cloud_horn_url']

    global task_cloud_horns_dict
    task_cloud_horns_dict = data['task_cloud_horns_dict']

    global camIdCloudHornDict
    camIdCloudHornDict = data['camIdCloudHornDict']

    global cloudhorn_sleep_time
    cloudhorn_sleep_time = data['cloudhorn_sleep_time']

    global alarmMsg


# 超过6天即重新申请
def get_accesstoken(cid):
    """
    通过摄像枪id对应绑定的用户id，secret获取accesstoken
    :param mid: 摄像枪序列号编码
    :return:
    """
    access_token = "no_access_token"
    client = str(camManageDict.get(cid))
    if client is None or len(client) <= 0:
        logging.info(" 摄像枪id: " + cid + " 未绑定对应的用户账号信息...")
        return access_token

    url = "https://www.cloud-dahua.com/gateway/auth/oauth/token"
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    client_info = client.split(',')
    formdata = {'client_id': str(client_info[0]),
                'client_secret': str(client_info[1]),
                'grant_type': 'client_credentials',
                'scope': 'server'
                }
    data = parse.urlencode(formdata, doseq=True)

    if clientAccessToken is not None:
        accesstokeninfo = clientAccessToken.get(client)
        current_time = str((datetime.datetime.now(pytz.timezone('Asia/Shanghai'))
                            + datetime.timedelta(days=6)).strftime("%Y-%m-%d %H:%M:%S"))
        access_token_time = current_time
        if accesstokeninfo is not None and accesstokeninfo != '':
            # 截取accesstoken的时间戳，查看是否超过6天，超过则重新获取，并更新camAccessToken信息
            accesstoken = accesstokeninfo.split(',')
            access_token_time = accesstoken[1]
            access_token = accesstoken[0]

        if accesstokeninfo is None or accesstokeninfo == '' \
                or (access_token_time is not None and access_token_time <= current_time):
            # 获取accesstoken 并打上服务器时间戳
            access_token = "no_access_token"
            content = requests.post(url=url, headers=headers, data=data).text
            content = json.loads(content)
            if content is not None and isinstance(content, dict) is True:
                if content.__contains__('token_type') is True and content.__contains__('access_token') is True:
                    valid_time = datetime.datetime.now(pytz.timezone('Asia/Shanghai')) + datetime.timedelta(days=6)
                    access_token = content['token_type'] + " " + content['access_token']
                    clientAccessToken[client] = str(access_token + "," + valid_time.strftime("%Y-%m-%d %H:%M:%S"))

    if access_token == "no_access_token":
        logging.info(" 摄像枪id: " + cid + " 通过用户鉴权信息: " + client + " 无法获取token...")
    else:
        logging.info(" 摄像枪id: " + cid + " 通过用户鉴权信息: " + client + " 获取token: " + access_token)
    return access_token


def msg_producer(q, msg):
    q.put(str(msg))
    idinfo = msg.split(",")
    logging.info(" 读取到云平台发送的消息--摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1]
                 + " ,消息id: " + idinfo[2] + " ...")
    q.join()  # 阻塞生产者线程，只有接收到消费者发送来的已经消费了最后一个产品的时候，才解除阻塞
    logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] + " 转入处理队列成功...")


def msg_consumer(q, p):
    while True:
        if not q.empty():
            msg = str(q.get())
            p.put(msg)
            idinfo = msg.split(",")
            logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] + " 转入处理队列中...")
        q.task_done()  # 向生产者发送消息，告诉生产者我已经消费了一个产品


def msg_process(p, alarmmsg):
    while True:
        if not p.empty():
            msg = str(p.get())
            idinfo = msg.split(",")
            logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: "
                         + idinfo[2] + " 进行告警信息获取所需的条件检查...")

            # 检查摄像枪是否绑定了用户id和secret
            client = str(camManageDict.get(idinfo[0]))
            if client is None or len(client) <= 0:
                logging.info(" 摄像枪id: " + idinfo[0] + " 未绑定对应的用户账号信息...")
                continue

            # 检查摄像枪是否与视频云的枪id信息对应
            gmcc_id_info = str(camIdDict.get(idinfo[0]))
            if gmcc_id_info is None or len(gmcc_id_info) <= 0:
                logging.info(" 摄像枪id: " + idinfo[0] + " 未绑定视频云的摄像枪id设备信息...")
                continue

            # 检查告警内容是否与视频云的告警内容相匹配
            gmcc_alarm_type = str(dahuaMsgType.get(idinfo[1]))
            if gmcc_alarm_type is None or len(gmcc_alarm_type) <= 0:
                logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " 无匹配的视频云的告警信息类型...")
                continue

            if idinfo[2].startswith("uuid:"):
                content = {'code': '0', 'data': alarmmsg[idinfo[2]], 'errMsg': '', 'success': True}
                logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] +
                             " 获取的告警信息: " + str(content))
            else:
                access_token = get_accesstoken(idinfo[0])
                if access_token != "no_access_token":
                    time.sleep(10)  # 休眠10秒后才进行消息获取
                    url = "https://www.cloud-dahua.com/gateway/messagecenter/api/messageInfo"
                    headers = {'Authorization': access_token, 'Content-Type': 'application/json;charset=utf-8'}
                    payload = {'messageId': str(idinfo[2])}
                    data = json.dumps(payload)
                    content = requests.post(url=url, headers=headers, data=data).text
                    content = json.loads(content)
                    logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] +
                                 " 获取的告警信息: " + str(content))

            if content is not None and isinstance(content, dict) is True:
                if content.__contains__('code') is True and content.__contains__('data') is True \
                        and content.__contains__('success') is True:
                    if content['code'] == "0" and content['success'] is True:
                        alarm_timestamp = content['data']['time'] / 1000.0  # 使用的是毫秒，将毫秒转为秒
                        alarm_date = str(datetime.datetime.fromtimestamp(alarm_timestamp)
                                         .strftime("%Y-%m-%d %H:%M:%S"))
                        # alarm_date = str(datetime.datetime.now(pytz.timezone('Asia/Shanghai'))
                        #                  .strftime("%Y-%m-%d %H:%M:%S"))

                        picList = []
                        for key in content['data']:
                            if str(key).lower() == "picurlarray":
                                picList = content['data'][key]
                                break

                        if picList is None or len(picList) <= 0:
                            logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] +
                                         " 无法获取相应的图片链接...")
                            alarm_pics = "http://nopic.jpg"
                        elif len(picList) == 1:
                            alarm_pics = str(picList[0])  # 只有一张大图
                        else:
                            alarm_pics = str(picList[1])  # 1是大图

                        gmcc_ids = gmcc_id_info.split(",")  # 视频云摄像枪的具体id信息

                        headers = {'content-type': 'application/json'}
                        gmcc_url = gmcc_url_port + "//ecs/v1/api/alarm/report"
                        if alarm_pics == "http://nopic.jpg": #没有获取到图片的情况
                            alarm_data = {"devId": gmcc_ids[0],  # 所属摄像头编号
                                          "vcuId": gmcc_ids[1],  # 所属设备编号
                                          "alarmType": gmcc_alarm_type,  # 告警类型
                                          "alarmTime": alarm_date,  # 告警时间
                                          "alarmState": "1",
                                          "alarmLevel": "1",
                                          "alarmLevelName": "",
                                          "presetId": "",
                                          "presetDesc": "",
                                          "uploadSnapFlag": "0",
                                          "alarmSnapUrl": "",
                                          "alarmGroupType": "2",
                                          "addition": msg
                                          }
                            gmcc_data = json.dumps(alarm_data)
                            content = requests.post(url=gmcc_url, headers=headers, data=gmcc_data).text
                            logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] +
                                         " 推送移动视频云告警信息的返回结果: " + str(content))
                        else:
                            alarm_data = {"devId": gmcc_ids[0],  # 所属摄像头编号
                                          "vcuId": gmcc_ids[1],  # 所属设备编号
                                          "alarmType": gmcc_alarm_type,  # 告警类型
                                          "alarmTime": alarm_date,  # 告警时间
                                          "alarmState": "1",
                                          "alarmLevel": "1",
                                          "alarmLevelName": "",
                                          "presetId": "",
                                          "presetDesc": "",
                                          "uploadSnapFlag": "2",
                                          "alarmSnapUrl": "",
                                          "alarmGroupType": "2",
                                          "addition": msg
                                          }
                            gmcc_data = json.dumps(alarm_data)
                            gmcc_url = gmcc_url_port + "//ecs/v1/api/alarm/report"
                            content = requests.post(url=gmcc_url, headers=headers, data=gmcc_data).text
                            if content is None or content.__contains__('snapUploadUrl') is False:
                                logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] +
                                             " 推送移动视频云告警信息后没有获取的返回上传图片接口信息: " + str(content))
                            else:
                                # 拿到上传图片文件的url
                                upload_img_dict = eval(content)
                                upload_url = upload_img_dict['snapUploadUrl']

                                # 获取图片的二进制流保存成本地文件
                                response = requests.get(alarm_pics)
                                binary_content = BytesIO(response.content).read()
                                img_data = {'file': (idinfo[2] + '.jpg', binary_content)}
                                encode_data = encode_multipart_formdata(img_data)
                                img_data = encode_data[0]
                                upload_header = {'Content-Type': encode_data[1]}
                                content = requests.post(url=upload_url, headers=upload_header, data=img_data).text
                                logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " ,消息id: " + idinfo[2] +
                                             " 推送移动视频云，上传图片完成后，告警信息的返回结果: " + str(content))

                        # 清空已经处理的消息
                        if idinfo[2].startswith("uuid:"):
                            del alarmmsg[idinfo[2]]

                        # 1.检查云喇叭是否绑定了url信息
                        if cloud_horn_url is None or len(cloud_horn_url) <= 0:
                            logging.info(" 当前没有配置任何云喇叭url前缀信息...")
                            continue

                        # 2.检查云喇叭是否配置了任务和云喇叭之间的映射信息
                        if task_cloud_horns_dict is None or len(task_cloud_horns_dict) <= 0:
                            logging.info(" 当前没有配置任何云喇叭广播任务信息...")
                            continue

                        cloud_alarm_type = str(camIdCloudHornDict.get(idinfo[0]))
                        if cloud_alarm_type is None or len(cloud_alarm_type) <= 0 \
                                or cloud_alarm_type.__contains__(idinfo[1]) is False:
                            logging.info(" 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] + " 无匹配的云喇叭的告警任务...")
                        else:
                            # 3.获取摄像枪配置的告警类型所映射的云喇叭任务id信息
                            cloud_alarm_pairs = cloud_alarm_type.split(";")
                            for cloud_alarm in cloud_alarm_pairs:
                                if len(cloud_alarm) > 0 and cloud_alarm.__contains__(idinfo[1]) is True:
                                    cloud_alarm_prefix = idinfo[1] + ","
                                    cloud_alarm_id = cloud_alarm.replace(cloud_alarm_prefix, "")  # 得到云喇叭播放任务id

                                    # 3.1 检查摄像枪对应的云喇叭任务id，是否有相应的云喇叭信息
                                    if task_cloud_horns_dict.__contains__(cloud_alarm_id) is False:
                                        logging.info(" 摄像枪id: " + idinfo[0] + ",消息类型: " + idinfo[1]
                                                     + " 对应的云喇叭播放任务: " + cloud_alarm_id
                                                     + " 没有匹配到云喇叭无法进行播放...")
                                        continue

                                    task_cloud_horns = task_cloud_horns_dict[cloud_alarm_id]
                                    cloud_horns = task_cloud_horns.split(";")
                                    for cloud_horn in cloud_horns:
                                        if len(cloud_horn) > 0 and cloud_horn.__contains__(',') is True:
                                            cloud_horn_id_info = cloud_horn.split(",")

                                            # 3.2 检查获取的每个云喇叭是否空闲，消息是否可以正常发送
                                            times = 0
                                            st_flag = False
                                            while times < 4:
                                                devsts = cloudlaba.post_get_terms_status_by_ids(cloud_horn_url, str(
                                                    cloud_horn_id_info[1]), str(cloud_horn_id_info[2]), str(
                                                    cloud_horn_id_info[3]))

                                                if devsts.__contains__('data') is False and \
                                                        devsts.__contains__('tstatus') is False:
                                                    break

                                                dev_status_dict = eval(devsts)
                                                # 终端状态：0-离线 1-未知 2-空闲 3-工作 4-故障
                                                if dev_status_dict['data'][0]['tstatus'] == 2:
                                                    times = 0
                                                    st_flag = True
                                                    break

                                                times += 1
                                                time.sleep(cloudhorn_sleep_time)  # 如果状态不是2，说明设备不可用，休眠15秒之后再执行

                                            if times >= 4 or st_flag is False:
                                                logging.info(" 摄像枪id: " + idinfo[0] + ",消息类型: " + idinfo[1]
                                                             + " 对应推送的云喇叭设备和id: " + cloud_horn_id_info[0]
                                                             + "," + cloud_horn_id_info[3]
                                                             + " 处于不可用状态，云喇叭无法进行播放...")
                                                continue

                                            result = cloudlaba.post_start_task(cloud_horn_url, cloud_horn_id_info[1],
                                                                               cloud_horn_id_info[2], cloud_alarm_id)
                                            logging.info("云喇叭任务id:" + cloud_alarm_id + " 启动返回结果: " + str(result))
                                            time.sleep(cloudhorn_sleep_time)  # 固定休眠一段时间后，再结束任务
                                            result = cloudlaba.post_stop_task(cloud_horn_url, cloud_horn_id_info[1],
                                                                              cloud_horn_id_info[2], cloud_alarm_id)
                                            logging.info("云喇叭任务id:" + cloud_alarm_id + " 结束返回结果: " + str(result))
                                            logging.info(
                                                " 摄像枪id: " + idinfo[0] + " ,消息类型: " + idinfo[1] +
                                                ", 已向云喇叭任务: " + cloud_alarm_id + "完成告警发出动作...")
        else:
            minute = sleep_time / 60.0
            logging.info(" 消息队列为空，暂无需转发数据，线程休眠" + str(minute) + "分钟")
            time.sleep(sleep_time)  # 队列为空则每sleep_time秒【可配置】读取一次队列信息，并尝试进行告警信息获取和转发


# 正式代码段：消息请求处理
@app.route('/dahuayunrui/callback', methods=['GET', 'POST'])  # 设置消息传递接收
def msg2gmccai():
    res_json = {"error_code": '500', "error_msg": '', "image": 'None', "result": [], "count": 0}

    try:
        if request.method == 'POST':
            # 请求方法是POST, 获取消息体
            msg_data = request.get_data()
            json_data = json.loads(msg_data)
            msg_type = json_data['msgType']
            cam_id = json_data['deviceId']
            # 记录大华返回的消息信息
            logging.info(json_data)
            if msg_type in dahuaMsgType and cam_id in camIdDict:
                # 1.检查是否带有messageId
                if "messageId" not in json_data:
                    # 没有消息id的情况，直接随机生成一个消息id
                    msg_id = "uuid:" + str(uuid.uuid1())
                    alarmMsg[msg_id] = json_data
                else:
                    msg_id = json_data['messageId']
                msg = cam_id + "," + msg_type + "," + msg_id
                pro = Thread(target=msg_producer, args=(msgQueue, msg))
                con = Thread(target=msg_consumer, args=(msgQueue, msgProcess))
                con.setDaemon(True)
                pro.start()
                con.start()
                pro.join()  # 阻塞当前所在的线程
                res_json["error_code"] = '200'
                res_json["error_msg"] = 'msg2gmccai runs successfully...'

            else:
                res_json["error_msg"] = 'msg2gmccai msg type: ' + str(msg_type) + 'is not need to process'
                logging.info(res_json)

        else:
            res_json["error_msg"] = 'msg2gmccai error: http request method is not POST'
            logging.info(res_json)
    except Exception as err:
        res_json["error_msg"] = 'msg2gmccai get http post error:' + str(err)
        logging.info(res_json)
    finally:
        return json.dumps(res_json, ensure_ascii=False)


# 入口函数
if __name__ == '__main__':
    # 运行app 指定IP 指定端口
    print(" 读取配置文件中的信息 ")
    readYaml()
    print(" 启动消息告警获取线程 ")
    # 测试代码段：
    # msgProcess.put("7J08E65PAG0F716,aiPerArea,dd19295c-532d-4886-ba26-3a196801d76e")
    process = Thread(target=msg_process, args=(msgProcess, alarmMsg))
    process.setDaemon(True)
    process.start()
    print(" 启动web接口")
    app.run("0.0.0.0", port=20090, threaded=False, debug=False)

# 测试代码段：
# readYaml()
# get_accesstoken("7J08E65PAG0F716")
# msgProcess.put("7J08E65PAG0F716,aiPerArea,dd19295c-532d-4886-ba26-3a196801d76e")
# msg_process(msgProcess)
